//
// GPSMapEdit
// (c) Konstantin Galichsky (kg@geopainting.com), 2005-2007
// Holux is registered trademark of Holux Technology Inc.
// ALAN and ALAN Map 600 are registered trademarks of ALAN Group.
//
// Holux ARD map parser.
//

# include "StdAfx.h"
# include "HoluxArd.h"
# include "Map.h"
# include "Types.h"
# include "HoluxTypes.h"
# include "HoluxRound.h"
# include "Load.h"
# include "Log.h"
# include "Status.h"
# include "ParseHelpers.h"
# include "HoluxCrdLoader.h"
# include "Wrappers.h"
# include "Globals.h"

class CArdLoader : public CCrdLoader {
  public:
	// Override.
	virtual LoadStatus_t Load (const char * _pData, size_t _cFileSize, const char * _strFileName, map_t * _pMap);

  protected:
	bool _IsSBN (const char * _pData, size_t _cFileSize) const {
		const CArdHdr * const pHdr = reinterpret_cast<const CArdHdr *> (_pData);
		const CArdHdr2 * pHdr2_1 = reinterpret_cast<const CArdHdr2 *> (_pData + sizeof (CArdHdr) + sizeof (DWORD));
		const bool bSBN = pHdr2_1->dwReserved3 == 0 && pHdr2_1->dwReserved4 == 0 && pHdr2_1->dwReserved5 == 0 && pHdr2_1->dwReserved6 == 0;

		return bSBN;
	}
};

class CHoluxArdLoader : public CArdLoader {
  public:
	virtual bool IsValid (const char * _pData, size_t _cFileSize) const {
		if (! CArdLoader::IsValid (_pData, _cFileSize))
			return false;
		return _IsSBN (_pData, _cFileSize);
	}
};

class CAlanArdLoader  : public CArdLoader {
  public:
	virtual bool IsValid (const char * _pData, size_t _cFileSize) const {
		if (! CArdLoader::IsValid (_pData, _cFileSize))
			return false;
		return ! _IsSBN (_pData, _cFileSize);
	}
};

DECLARE_MAP_LOADER (CHoluxArdLoader, "holux-ard", "ard", "Holux MapRoute map (*.ard)");
DECLARE_MAP_LOADER (CAlanArdLoader,  "alan-ard",  "ard", "ALAN Map 600 map (*.ard)");

LoadStatus_t CArdLoader::Load (const char * _pData, size_t _cFileSize, const char * _strFileName, map_t * _pMap) {
	m_pMap = _pMap;

	m_pData = _pData;
	m_cFileSize = _cFileSize;
	const char * pEnd  = _pData + _cFileSize;

	m_file_refs.clear ();
	m_file_refs.push_back (file_ref_t ());
	m_file_refs.back ().strFileName = _strFileName;
	m_file_refs.back ().bAttachment = false;

	SetProgressStatus (0);

	//
	// Parse the header.
	//
	const CArdHdr * const pHdr = reinterpret_cast<const CArdHdr *> (_pData);
	if ((pHdr->dwSubadmDataOffset - pHdr->dwAdmDataOffset) != pHdr->wAdmNum*sizeof (CCrdAdm))
		ReportWarning ("Probably data in the file header are damaged.");

	const CArdHdr2 * pHdr2_0 = reinterpret_cast<const CArdHdr2 *> (_pData + sizeof (CArdHdr));
	const CArdHdr2 * pHdr2_1 = reinterpret_cast<const CArdHdr2 *> (_pData + sizeof (CArdHdr) + sizeof (DWORD));
	const bool b0 = pHdr2_0->dwReserved3 == 0 && pHdr2_0->dwReserved4 == 0 && pHdr2_0->dwReserved5 == 0 && pHdr2_0->dwReserved6 == 0;
	const bool b1 = pHdr2_1->dwReserved3 == 0 && pHdr2_1->dwReserved4 == 0 && pHdr2_1->dwReserved5 == 0 && pHdr2_1->dwReserved6 == 0;
	const bool bSBN = b1;
	const CArdHdr2 * const pHdr2 = bSBN ? pHdr2_1 : pHdr2_0;

	m_pMap->cHeaderLen = sizeof (CArdHdr);
	m_pMap->bNoHeader  = false;
	m_pMap->pTypeSet   = & g_tsHolux;
	m_pMap->Header.ElevationUnits = unMeters;

	// Country.
	char strCountry [20];
	::sprintf (strCountry, "%d", pHdr->wCountry);
	m_pMap->Countries.push_back (country_t ());
	country_t & country = m_pMap->Countries.back ();
	country.strName = strCountry;
	if (pHdr->wCountry > 255)
		ReportWarning ("Unexpected country code in header.");

	// Language.
	switch (pHdr2->btLanguage) {
		case clAscii7:
			m_pMap->Header.wCodePage = 0;
			m_pMap->Header.CodeSchema = bpsAscii7;
			break;

		case clSBCS:
			m_pMap->Header.wCodePage = 1252; // Latin-I
			m_pMap->Header.CodeSchema = bpsSBCS;
			break;

		case clMBCSTaiwan:
			m_pMap->Header.wCodePage = 950;  // Traditional Chineese
			m_pMap->Header.CodeSchema = bpsMBCS;
			break;

		case clMBCSKorean:
			m_pMap->Header.wCodePage = 949;
			m_pMap->Header.CodeSchema = bpsMBCS;
			break;

		default:
			assert (false);
			break;
	}

	//
	// Parse administrations.
	//
	std::map<DWORD, region_t *> mapRegions;
	typedef std::map<DWORD, city_t *> mapCities_t;
	mapCities_t mapCities;
	size_t cCity = 0;

	const CCrdAdm * const pAdms = reinterpret_cast<const CCrdAdm *> (_pData + pHdr->dwAdmDataOffset);
	for (size_t cAdm = 0; cAdm < pHdr->wAdmNum; ++ cAdm) {
		m_pMap->Regions.push_back (region_t ());
		region_t & region = m_pMap->Regions.back ();
		region.pCountry = & country;

		const CCrdAdm & adm = pAdms [cAdm];
		assert (adm.dwOrdinal == cAdm + 1);

		ReadString (region.strName, adm.Name);

		mapRegions [adm.dwOrdinal] = & region;

		// Parse subadministrations.
		const CCrdSubadm * const pSubadms = reinterpret_cast<const CCrdSubadm *> (_pData + adm.dwSubadmOffset);
		for (size_t cSubadm = 0; cSubadm < adm.wSubadmNum; ++ cSubadm) {
			m_pMap->Cities.push_back (city_t ());
			city_t & city = m_pMap->Cities.back ();
			cCity ++;

			const CCrdSubadm & subadm = pSubadms [cSubadm];
			assert (subadm.dwOrdinal == cCity + pHdr->wAdmNum);

			ReadString (city.strName, subadm.Name);
			city.pRegion = & region;

			mapCities [subadm.dwOrdinal] = & city;
		}
	}

	//
	// Parse the search index data.
	//
	const CCrdSearchItem * const pSearch = reinterpret_cast<const CCrdSearchItem *> (_pData + pHdr->dwSubadmDataOffset + pHdr->wSubadmNum*sizeof (CCrdSubadm));

# if defined (_DEBUG)
	size_t cSearchNum = 0;
	size_t cType;
	for (cType = 0; cType < c_cCrdTypes; ++ cType) {
		const CCrdSearchItem & siAdm = pSearch [cType*(pHdr->wSubadmNum + 1)];

		assert (siAdm.wAdmId == 0 || siAdm.wAdmId == 1);

		size_t cSubSum = 0;
		DWORD dwFirstIndex = 0;
		bool bScanForFirstNonEmptySubadm = true;
		for (size_t cSubAdm = 0; cSubAdm < pHdr->wSubadmNum; ++ cSubAdm) {
			const CCrdSearchItem & si = pSearch [cType*(pHdr->wSubadmNum + 1) + 1 + cSubAdm];

			assert (si.wAdmId == 0 || si.wAdmId == cSubAdm + 2);

			cSearchNum += si.wNum;
			cSubSum    += si.wNum;

			if (bScanForFirstNonEmptySubadm && si.wNum != 0) {
				bScanForFirstNonEmptySubadm = false;
				dwFirstIndex = si.dwIndex;
			}
		}

		assert (siAdm.dwIndex == dwFirstIndex);
		assert (siAdm.wNum    == cSubSum);
	}
	assert (cSearchNum == pHdr->dwIndexSize);

	assert (pHdr->dwIndexOffset == pHdr->dwSubadmDataOffset + pHdr->wSubadmNum*sizeof (CCrdSubadm) + c_cCrdTypes*(pHdr->wSubadmNum + 1)*sizeof (CCrdSearchItem));
	const char * const pIndexStart = _pData + pHdr->dwIndexOffset;
	const char * const pIndexEnd = pIndexStart + pHdr->dwIndexSize*sizeof (CCrdIndexItem) + c_cCrdTypes*sizeof (DWORD);

//# define _CHECK_ORDER
	cType = 0;
	int iIndex = 0;
	const char * pIndex = pIndexStart;
	while (pIndex < pIndexEnd) {
		// Prepare for next type.
		DWORD dwPrevAdmId = -1;

		string_t strPrevLabel;
		string_t strPrevFirstLetter;

		bool bFirstLetterReached = true;
		bool bLastLetterReached  = false;

		int iLastPrev = -1;
		int iLastNext = -1;
		int iLastCur  = iIndex;

		int iFirstIndex = iIndex;

		const size_t cItems = * reinterpret_cast<const DWORD *> (pIndex);
		pIndex += sizeof (DWORD);

		assert (cType < c_cCrdTypes);
		assert (pSearch [cType*(pHdr->wSubadmNum + 1)].wNum == cItems);

		const CCrdIndexItem * const pItems = reinterpret_cast<const CCrdIndexItem *> (pIndex);
		for (size_t cItem = 0; cItem < cItems; ++ cItem) {
			const CCrdIndexItem & ii = pItems [cItem];

			assert (ii.dwAdmId != 0);
			const bool bNextAdmStarted = ii.dwAdmId != dwPrevAdmId;
			if (bNextAdmStarted) {
				assert (dwPrevAdmId == -1 || ii.dwAdmId >= dwPrevAdmId + 1);

				const CCrdSearchItem & si = pSearch [cType*(pHdr->wSubadmNum + 1) + (ii.dwAdmId - 1)];
				assert (si.dwIndex == (DWORD) iIndex);

				strPrevLabel.clear ();
				strPrevFirstLetter.clear ();

				bFirstLetterReached = true;
				bLastLetterReached  = false;

				iLastPrev = -1;
				iLastNext = -1;
				iLastCur  = iIndex;

				iFirstIndex = iIndex;
			}

			string_t strLabel;
			if (ii.dwNameIdx >= pHdr->dwIndexOffset && ii.dwNameIdx < _cFileSize && ii.dwNameLen)
				strLabel.assign (_pData + ii.dwNameIdx, ii.dwNameLen);

			const int iPrev = iIndex - ii.shBackward;
			if (ii.shBackward >= 0)
				assert (iPrev < (int) pHdr->dwIndexSize);

			const int iNext = iIndex + ii.shForward;
			if (iNext >= 0)
				assert ((size_t) iNext < pHdr->dwIndexSize);

			string_t strFirstLetter;
			if (! strLabel.empty ()) {
				const char * const pFirstLetter = strLabel.c_str ();
				const char * const pSecondLetter = ::CharNext (pFirstLetter);
				strFirstLetter.assign (pFirstLetter, pSecondLetter);
			}

			if (! strPrevFirstLetter.empty () && strFirstLetter == strPrevFirstLetter) {
# ifdef _CHECK_ORDER
				if (ii.shBackward >= 0)
					assert (iLastPrev == iPrev);

				if (ii.shForward > 0)
					assert (iLastNext == iNext);

				assert (
					::strcmp  (strPrevLabel.c_str (), strLabel.c_str ()) <= 0 ||
					::strcmpi (strPrevLabel.c_str (), strLabel.c_str ()) <= 0
				);
# endif // _CHECK_ORDER
			} else {
# ifdef _CHECK_ORDER
				if (bFirstLetterReached)
					assert (ii.shBackward <= 0 || iPrev < iFirstIndex);
				else {
					assert (iPrev  == iLastCur);
					assert (iIndex == iLastNext);
				}

				if (ii.shForward != 0) {
					assert (bLastLetterReached == false);
					assert (iLastNext != iNext);
				} else
					bLastLetterReached = true;

				if (! strPrevFirstLetter.empty () || ! strFirstLetter.empty ())
					assert (
						::strcmpi (strPrevFirstLetter.c_str (), strFirstLetter.c_str ()) <= 0 ||
						::strcmp  (strPrevFirstLetter.c_str (), strFirstLetter.c_str ()) <= 0
					);
# endif // _CHECK_ORDER
				strPrevFirstLetter.swap (strFirstLetter);

				iLastCur = iIndex;
				if (! strPrevFirstLetter.empty ())
					bFirstLetterReached = false;
			}
			iLastPrev = iPrev;
			iLastNext = iNext;
			strPrevLabel.swap (strLabel);
			dwPrevAdmId = ii.dwAdmId;

			++ iIndex;
		}
		pIndex += cItems*sizeof (CCrdIndexItem);

		++ cType;
	}
	assert (pIndex == pIndexEnd);
# endif // _DEBUG

	assert (pHdr2->dwMapHdrOffset == pHdr->dwIndexOffset + pHdr->dwIndexSize*sizeof (CCrdIndexItem) + c_cCrdTypes*sizeof (DWORD));

	//
	// Parse the map area header.
	//
	const char * const pMapHdr_ = _pData + pHdr2->dwMapHdrOffset;
	const CArdMapHdr * const pMapHdr = reinterpret_cast<const CArdMapHdr *> (pMapHdr_);
	const CArdMapHdr2 * const pMapHdr2 = reinterpret_cast<const CArdMapHdr2 *> (pMapHdr_ + sizeof (CArdMapHdr) + (bSBN ? 2*sizeof (DWORD) : 0));

	// Name.
	m_pMap->Header.strName = pMapHdr->strName;

	// Id.
	char buf_id [20];
	::sprintf (buf_id, "%d", pMapHdr->dwMapId);
	m_pMap->Header.strID = buf_id;

	// Datum.
	if (pMapHdr->btDatum != 0)
		ReportWarning ("Some unsupported datum (#%d) other than WGS 84 is used. This may cause some coordinate shift.", pMapHdr->btDatum);

	assert (pMapHdr2->lX0 < pMapHdr2->lX1);
	assert (pMapHdr2->lY0 < pMapHdr2->lY1);

	const CCrdScale * const pScales = reinterpret_cast<const CCrdScale *> (pMapHdr_ + pMapHdr->dwScalesOffset);
	const CArdBlock * const pBlocks = reinterpret_cast<const CArdBlock *> (pMapHdr_ + pMapHdr->dwBlocksOffset);
	const CCrdType  * const pTypes  = reinterpret_cast<const CCrdType  *> (pMapHdr_ + pMapHdr->dwTypesOffset);

	const CArdPolyline * const pPolylines = reinterpret_cast<const CArdPolyline *> (pMapHdr_ + pMapHdr->dwPolylinesOffset);
	const CArdPolygon  * const pPolygons  = reinterpret_cast<const CArdPolygon  *> (pMapHdr_ + pMapHdr->dwPolygonsOffset);
	const CArdPoint    * const pPoints    = reinterpret_cast<const CArdPoint    *> (pMapHdr_ + pMapHdr->dwPointsOffset);

	const CCrdPt * const pPolygonPts  = reinterpret_cast<const CCrdPt *> (pMapHdr_ + pMapHdr->dwPolygonPartOffset);
	const CCrdPt * const pPolylinePts = reinterpret_cast<const CCrdPt *> (pMapHdr_ + pMapHdr->dwPolylinePartOffset);

	const CArdNode * const pNodes = reinterpret_cast<const CArdNode *> (pMapHdr_ + pMapHdr->dwNodesOffset);
	const CArdArc  * const pArcs  = reinterpret_cast<const CArdArc  *> (pMapHdr_ + pMapHdr2->dwArcsOffset);

	m_pMap->Levels.resize (pMapHdr->btScaleNum + 1);
	m_pMap->Levels [pMapHdr->btScaleNum].cZoom = pMapHdr->btScaleNum;
	m_pMap->Levels [pMapHdr->btScaleNum].SetBasicScale (24 - 3*pMapHdr->btScaleNum);

	for (size_t cScale = 0; cScale < pMapHdr->btScaleNum; ++ cScale) {
		m_pMap->Levels [cScale].cZoom = cScale;
		m_pMap->Levels [cScale].SetBasicScale (24 - 3*cScale);

		const CCrdScale & scale = pScales [cScale];

		assert (scale.dwCols*scale.dwRows == scale.dwBlocks);
		assert (scale.dwBlockWidth *scale.dwCols >= size_t (pMapHdr2->lX1 - pMapHdr2->lX0));
		assert (scale.dwBlockHeight*scale.dwRows >= size_t (pMapHdr2->lY1 - pMapHdr2->lY0));

# ifdef _DEBUG
		ReportText ("* Zoom level #%d: %d=%dx%d blocks, each %dx%d size.", cScale, scale.dwBlocks, scale.dwCols, scale.dwRows, scale.dwBlockWidth, scale.dwBlockHeight);
# endif // _DEBUG

		for (size_t cBlock = 0; cBlock < scale.dwBlocks; ++ cBlock) {
			const CArdBlock & block = pBlocks [scale.dwFirstBlockIdx + cBlock];

			if (cScale != 0)
				assert (block.dwNodes == 0);

			const int nx = cBlock % scale.dwCols;
			const int ny = cBlock / scale.dwCols;
			const int nBlockX = scale.lX0 + nx*scale.dwBlockWidth;
			const int nBlockY = scale.lY0 + ny*scale.dwBlockHeight;

			for (size_t cType = 0; cType < block.dwTypes; ++ cType) {
				const CCrdType & type = pTypes [block.dwFirstTypeIdx + cType];

				for (size_t cElem = 0; cElem < type.dwElems; ++ cElem) {
					RGN_KIND rk;
					USHORT ushType = 0;
					Holux2Garmin (type.dwElemType, rk, ushType);
					ushType = type.dwElemType;
					if (rk == rkPolygon) {
						const CArdPolygon & poly = pPolygons [type.dwFirstElemIdx + cElem];

						// Label.
						string_t strLabel;
						ReadString (strLabel, poly.Label);

						// Points.
						points_t points (poly.dwPoints);
						for (size_t cPoint = 0; cPoint < poly.dwPoints; ++ cPoint) {
							const CCrdPt & pt = pPolygonPts [poly.dwFirstPointIdx + cPoint];
							assert (pt.wX <= poly.wWidth);
							assert (pt.wY <= poly.wHeight);
							points [cPoint].x =  (poly.lX0 + pt.wX)*c_fHoluxCell;
							points [cPoint].y = -(poly.lY1 + pt.wY)*c_fHoluxCell;
						}

						AddPoly (cScale, ushType, rk, points, strLabel, NULL, reinterpret_cast<const char *> (& poly) - _pData, sizeof (poly));
					} else if (rk == rkPolyline) {
						const CArdPolyline & poly = bSBN ?
							* (const CArdPolyline *) ((BYTE *) pPolylines + (type.dwFirstElemIdx + cElem)*(sizeof (CArdPolyline) + 2*sizeof (DWORD)))
						  :
							pPolylines [type.dwFirstElemIdx + cElem];

						// Label.
						string_t strLabel;
						ReadString (strLabel, poly.Label);

						// Points.
						points_t points (poly.wPoints);
						for (size_t cPoint = 0; cPoint < poly.wPoints; ++ cPoint) {
							const CCrdPt & pt = pPolylinePts [poly.dwFirstPointIdx + cPoint];
							assert (pt.wX <= poly.wWidth);
							assert (pt.wY <= poly.wHeight);
							points [cPoint].x =  (poly.lX0 + pt.wX)*c_fHoluxCell;
							points [cPoint].y = -(poly.lY1 + pt.wY)*c_fHoluxCell;
						}

						// City.
						const mapCities_t::const_iterator it = mapCities.find (poly.wAdmId);
						city_t * pCity = it == mapCities.end () ? NULL : it->second;

						AddPoly (cScale, ushType, rk, points, strLabel, pCity, reinterpret_cast<const char *> (& poly) - _pData, sizeof (poly));
					} else {
						assert (rk == rkPoint);
						const CArdPoint & point = pPoints [type.dwFirstElemIdx + cElem];

						assert (point.wBlockId == scale.dwFirstBlockIdx + cBlock);

						// Label & Phone.
						string_t strLabelAndPhone;
						ReadString (strLabelAndPhone, point.Label);
						string_t strLabel, strPhone;
						if (! strLabelAndPhone.empty ()) {
							const char * const pColon = ::strrchr (strLabelAndPhone.c_str (), ':');
							if (pColon) {
								strLabel.assign (strLabelAndPhone.c_str (), pColon);
								strPhone.assign (pColon + 1);
							} else
								strLabel.swap (strLabelAndPhone);
						}

						// Position.
						assert (point.wX <= scale.dwBlockWidth);
						assert (point.wY <= scale.dwBlockHeight);
						const float fX =  (nBlockX + point.wX)*c_fHoluxCell;
						const float fY = -(nBlockY + point.wY)*c_fHoluxCell;

						// City.
						const mapCities_t::const_iterator it = mapCities.find (point.wAdmId);
						city_t * pCity = it == mapCities.end () ? NULL : it->second;

						AddPoint (cScale, ushType, rk, fX, fY, strLabel, pCity, strPhone, reinterpret_cast<const char *> (& point) - _pData, sizeof (point));
					}
				}
			}
		}

		SetProgressStatus (50*(cScale + 1)/pMapHdr->btScaleNum);
	}

	m_pMap->file_refs.swap (m_file_refs);

	SetProgressStatus (50);

	return lsOK;
}
